Testaaminen on ohjelmistokehityksen perusjuttuja ja tapa varmistaa, että koodi jota tehdään, on toimivaa. Testaamista on monenlaista, tässä keskityn yksikkötestaamiseen (unit testing) ja integraatiotestaamiseen (integration testing). Näissä erona on, että yksikkötestauksessa testataan ohjelmiston yksittäisiä komponentteja ja integraatiotestauksessa isompia kokonaisuuksia. WordPress-kontekstissa näitä kahta on kuitenkin hankalaa ja tarpeetontakin erottaa tarkasti toisistaan. Kolmas olennainen testaamisen muoto on regressiotestaus (regression testing), jossa varmistellaan, että aikaisemmin toimivat ominaisuudet eivät ole menneet rikki.
Kirjoitus on laadittu Mac OS -käyttäjän näkökulmasta, sillä oletuksella että Homebrew on käytössä ja perustyökalut kuten wget ja Composer on asennettu. Linux-käyttäjät osannevat käyttää omaa paketinhallintaansa tarvittavien työkalujen asentamiseen, Windows-käyttäjät joudun valitettavasti jättämään oman onnensa nojaan.
Testaustyökalut
Tarkastelen testaamista ensisijaisesti lisäosakehittäjän näkökulmasta, koska se on oma näkökulmani. Periaatteet lienevät sovellettavissa muuallekin, etenkin sellaisiin sivustoprojekteihin, joihin sisältyy paljon omaa koodia, usein omana lisäosana toteutettuna.
PHPUnit. WordPressiä testattaessa perustyökalu on PHPUnit. Tätä kirjoittaessani uusinta versiota 8 ei vielä voi käyttää WordPressin testaamiseen, versio 7 sen sijaan toimii mainiosti. Aloita siis asentamalla PHPUnit.
Wp-test-framework. Jotta testejä voidaan ajaa, tarvitaan testiympäristö. Otto Rask on kehittänyt erittäin näppärän testipaketin, joka huolehtii kaikesta. Otetaan siis käyttöön wp-test-framework, joka tulee näppärästi käyttöön Composer-riippuvuutena:
composer require --dev rask/wp-test-framework
Kun riippuvuus on määritelty composer.json
-tiedostoon, framework asennetaan yksinkertaisesti ajamalla
composer update
Sen jälkeen framework on käytettävissä.
WordPress. Lisäksi tarvitaan WordPress. Sitä ei tarvitse edes asentaa, riittää että tiedostot ovat saatavilla jossain. Jos käytössä on WordPress CLI, asennus käy näin kätevästi:
wp core download
Ilman WP CLI:tä homma hoituu lähes yhtä näppärästi näin:
wget https://wordpress.org/latest.zip unzip latest.zip
WordPressin hakemistoon on vain lisättävä testiasetukset sisältävä wp-tests-config.php
-tiedosto, josta löytyy mallikappale WordPressin repositoriosta. Tähän tiedostoon täytyy vain päivittää tietokannan asetukset.
WP_TESTS_INSTALLATION
-ympäristömuuttuja säädetään osoittamaan tähän asennushakemistoon, esimerkiksi näin:
export WP_TESTS_INSTALLATION=/Users/msaari/wordpress/
MySQL-tietokanta. Mistä päästäänkin palapelin viimeiseen palaseen, eli käytössä on oltava tietokanta. Jos koneelta löytyy jo valmiiksi tietokantapalvelin (esimerkiksi MAMP:n muodossa), käytä sitä, muussa tapauksessa MariaDB on vaivaton asennettava:
brew install mariadb
mysql.server start
Tarkista, että wp-tests-config.php
-tiedostossa on oikeat tiedot tietokannalle. Huomaa, että testejä varten täytyy olla täysin oma tietokantansa, koska testien aluksi tietokannasta tuhotaan aina kaikki taulut. Samassa tietokannassa ei siis voi pitää mitään muuta.
PHPUnitin asetukset
Nyt kun testijärjestelmän osat ovat koossa, on aika rakennella varsinaisia testejä. Projektin juurihakemistoon luodaan tiedosto phpunit.xml
, jossa määritellään, miten testit ajetaan. Relevanssin asetukset näyttävät tältä:
<phpunit bootstrap="tests/bootstrap.php" backupGlobals="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" > <php> <ini name="display_errors" value="true"/> </php> <testsuites> <testsuite name="Relevanssi"> <directory prefix="test-" suffix=".php">./tests/</directory> <directory prefix="test-" suffix=".php">./premium-tests/</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix=".php">./lib</directory> <directory suffix=".php">./premium</directory> </whitelist> </filter> </phpunit>
Osa on ihan perusasetuksia, osa taas sitten projektikohtaista. Tärkeä on bootstrap
-asetus, jossa määritellään tiedosto, josta löytyy testien bootstrap-ohjeet eli se, miten järjestelmä käynnistetään. Siitä kohta lisää.
Tiedostossa määritellään testsuite
, jossa on määritelty, että testit löytyvät hakemistoista tests
ja premium-tests
tiedostoista, joiden nimet alkavat ”test-
”. Lopun whitelist
-filtteri liittyy testikattavuusasioihin, eli se kertoo, mistä hakemistoista löytyy koodi, josta testikattavuutta mitataan.
Bootstrap-tiedosto
Tiedoston tests/bootstrap.php
sisältö taas on yksinkertaisuudessaan tämä:
require_once './vendor/autoload.php'; \rask\WpTestFramework\Framework::load(); require_once dirname( __DIR__ ) . '/relevanssi.php';
Ensimmäiset kaksi riviä lataavat ja käynnistävät Raskin wp-test-frameworkin ja viimeinen rivi sisällyttää mukaan Relevanssin.
Mitään tavanomaista lisäosanasennusprosessia tässä ei ole käytössä, vaan tarvittavat lisäosat asennetaan näin, sisällyttämällä niiden tiedostot requirella
.
Testien kirjoittaminen
Testitiedostot ovat rakenteeltaan luokkia, jotka laajentavat WP_UnitTestCase
-luokkaa. Alkuvalmistelut tehdään julkisessa staattisessa funktiossa wpSetupBeforeClass()
, lopuksi ajetaan wpTearDownAfterClass()
ja siinä välissä suoritetaan kaikki julkiset funktiot, joiden nimi alkaa test
.
Relevanssin tapauksessa jokaisessa luokassa ajetaan ensin lisäosan asennusfunktio relevanssi_install()
ja normaalisti init
-koukussa ajettava relevanssi_init()
– koska sitä init
-koukkua ei testien yhteydessä suoriteta – ja sitten lopuksi siivotaan jäljet ajamalla relevanssi_uninstall()
. Näin eri testikokonaisuudet eivät häiritse toisiaan.
Luokat on jaoteltu sen mukaan, mitä toiminnallisuutta ne testaavat. Noin karkeasti jokainen luokka liittyy erilliseen tiedostoon. Omat luokkansa on muun muassa indeksoinnin, hakemisen, otteiden ja korostusten, käyttöliittymän, hukkasanojen ja taksonomiakyselyjen testaamiselle.
Yksittäisessä testifunktiossa testataan jotain tiettyä asiaa suorittamalla Relevanssin funktioita ja tekemällä oletuksia palautusarvoista. Tavallisin testioletus on $this->assertEquals()
, joka valittaa virheestä, jos vertailuarvo ei vastaa oletusta. Yksikkötestissä voidaan siis suorittaa jostain pienestä yksityiskohdasta vastaava funktio, jonka palautusarvo eri syötteillä tiedetään ja sitten vain vertaillaan, vastaako funktiosta tuleva palautusarvo sitä, mitä sen pitäisi olla.
Vinkkejä testien koodaamiseen
Jos kokonaisuus koostuu riittävän pienistä funktioista, yksikkötestien kirjoittaminen on melko suoraviivaista. Testaaminen kannustaakin pilkkomaan liian isoja funktioita pienempiin osiin, joita on sitten helpompi testata. Tällöin erilaisia alkuvalmisteluitakin tarvitaan vähemmän: yleensä riittää, että funktiolle annetaan oikeanlainen syöte.
WordPressin lisäosia testatessa tulee kuitenkin jonkin verran tarvetta laajemmille integraatiotesteille. Esimerkiksi Relevanssin kohdalla voidaan toki testata yksittäisiä funktioita, mutta on myös hyvä testata, että koko hakuprosessi menee läpi niinkuin pitää, tai että tietokannan indeksointi kokonaisuudessaan toimii. Se vaatii vähän alkuvalmisteluja ja sopivan sisällön rakentelemista tietokantaan.
Testifunktioissa on mahdollista luoda artikkeleita ja muuta sisältöä WordPressin funktioilla. Artikkelien luominen wp_insert_post()-funktiolla toimii aivan hyvin. WordPressin testipaketissa on myös jokunen apufunktio ja tehdas, joilla voi luoda sisältöä. Esimerkiksi monta artikkelia voi luoda kerralla funktiolla $this->factory->post->create_many( 10 )
, joka antaa paluuarvona listan luotujen artikkeleiden ID-tunnuksista. Näille ei ole olemassa dokumentaatiota, eli tarkempi perehtyminen asiaan edellyttää lähdekoodin tutkailua.
Testifunktioilla voi olla riippuvuuksia toisistaan. Jos funktiolle määrittelee riippuvuuden merkitsemällä kommenttiblokkiin @depends test_toinen_testi()
, tämä jälkimmäinen funktio suoritetaan ensimmäisen jälkeen ja se saa parametrinään edeltävän funktion return
-arvon. Kannattaa huomata, että funktiot voivat vaikuttaa toisiinsa muutenkin. Kaikki oletukset esimerkiksi asetuksien suhteen kannattaa ottaa huomioon ja kaikki jotain testiä varten säädetyt asetukset nollata testin jälkeen, muuten saa ihmetellä mikä on, kun joku testi menee läpi kun sen suorittaa yksinään, mutta ei osana isompaa kokonaisuutta.
Tässä on esimerkki funktiosta, joka testaa kuvaliitteiden indeksoinnin estävää asetusta.
/** * Tests the relevanssi_no_image_attachments function and feature. */ public function test_no_image_attachments() { $this->delete_all_posts(); $image_attachment = array( 'post_title' => 'cat gif', 'post_mime_type' => 'image/gif', 'post_type' => 'attachment', 'post_status' => 'publish', ); wp_insert_post( $image_attachment ); $pdf_attachment = array( 'post_title' => 'cat pdf', 'post_mime_type' => 'application/pdf', 'post_type' => 'attachment', 'post_status' => 'publish', ); wp_insert_post( $pdf_attachment ); update_option( 'relevanssi_index_post_types', array( 'attachment' ) ); update_option( 'relevanssi_index_image_files', 'off' ); $return = relevanssi_build_index( false, null, null, true ); $this->assertEquals( 1, $return['indexed'], "With image files excluded, the number of posts indexed isn't correct." ); update_option( 'relevanssi_index_image_files', 'on' ); $return = relevanssi_build_index( false, null, null, true ); $this->assertEquals( 2, $return['indexed'], "With image files included, the number of posts indexed isn't correct." ); }
Testissä poistetaan kaikki artikkelit $this->delete_all_posts()
-apufunktiolla, luodaan sitten kaksi artikkelia, kuvaliite ja PDF-muotoinen liite (varsinaisia liitetiedostoja ei ole, koska niillä ei ole merkitystä tämän testin kannalta), ja sen jälkeen säädetään Relevanssi indeksoimaan liitteet. Kuvaliitteiden indeksointi kytketään pois päältä ja tietokanta indeksoidaan relevanssi_build_index()
-funktiolla.
Jos kaikki menee oikein, $return['indexed']
eli indeksoitujen artikkelien lukumäärä on yksi. Sitten kuvaliitteiden indeksointi sallitaan ja uusitaan indeksointi, jolloin $return['indexed']
pitäisi saada arvokseen kaksi.
Mitä pitäisi testata?
Millaisia juttuja sitten kannattaa testata? Tätä ihmettelin aluksi kovasti. Päädyin rakentelemaan testejä tavallisimmille toiminnallisuuksille ja vastaan tulleille bugeille. Sitten opin, että on olemassa asia nimeltä testikattavuus, jolla voidaan tarkistaa, mitkä kaikki ohjelmakoodin osat on testattu, ja sen jälkeen olen ottanut asiakseni käydä kaiken koodin läpi niin, että testikattavuus olisi sataprosenttinen.
Tavoite ei ole ihan täysin mielekäs, mutta se on ollut kuitenkin hyödyllinen. Tämän tavoitteen saavuttamiseksi olen käynyt Relevanssin koodia läpi tiheällä kammalla, refaktoroinut sitä uuteen, järkevämpään malliin ja siinä sivussa löytänyt runsaasti bugeja ja puutteita esimerkiksi taksonomiakyselyistä – ne ovat hankala aihe, jossa on helppo tehdä virheitä, ja huolellinen ja perinpohjainen testaaminen on auttanut kaivamaan esiin monta bugia ja epäloogisuutta, joita kymmenen vuotta vanhaan koodimassaan on varmasti pesiytynyt joka puolelle.
Testikattavuusraportit saa tehtyä PHPUnitilla. Tarjolla on muutama erilainen malli, minä olen käyttänyt tätä:
phpunit --coverage-html coverage
Tämä tekee coverage
-hakemistoon HTML-muotoisen raportin, josta näkee miltä osin koodi on testattu ja mitä osia koodista ei suoriteta lainkaan. Raportteihin sisältyy Change Risk Anti-Patterns eli CRAP-indeksi (tuttavallisemmin paskaindeksi), joka perustuu syklomaattiseen kompleksisuuteen ja testikattavuuteen ja kertoo yhden näkemyksen siitä, miten vaikeaselkoista koodi on. Tästä saa olla monta mieltä, mutta oma näkemykseni on, että pienempi CRAP-lukema tarkoittaa selkeämpää ja helppotajuisempaa koodia ja on siksi hyvä asia. Kun testikattavuus lähestyy sataa prosenttia ja CRAP-lukema pienenee, koodista tulee miellyttävämpää.
Se mitä Relevanssin testipaketti ei lainkaan testaa tällä hetkellä on yhteensopivuudet muiden lisäosien kanssa. Tähänkin ehkä jossain kohtaa päästään, mutta siinä, että saa testiasennukseen sisällytettyä tarvittavat muut lisäosat ja viriteltyä ne koodin tasolla testaustilanteen vaatimaan kuntoon, on sen verran paljon työtä, että en ole siihen vielä lähtenyt.
Tarkemmin Relevanssin testikokonaisuutta voi tutkailla GitHub-reposta, josta löytyy ilmaisversion testit. Relevanssi Premiumin asennuspaketin mukana tulee täysi sarja testejä myös Premiumin ominaisuuksille. WordPress.orgista saatavassa ilmaisversion asennuspaketissa ei ole mitään testejä mukana.
Testaamisprosessi
Kuvasin aikaisemmin omaa kehitysprosessiani. Siihen testit asettuvat siten, että ensinnäkin kehitystyön aikana ajan testejä kehitysversiota vasten – kun jotain on tehty, tsekataan phpunitilla
että mitään ei mennyt rikki ja tarvittaessa lisätään uusia testejä uutta toiminnallisuutta varten. Varsinkin ihan uusia ominaisuuksia luotaessa testien kirjoittamisen pitäisi olla osa kehitystyötä.
Vielä enemmän testaustyötä tehdään silloin, kun uuden kehittämisen sijasta refaktoroidaan vanhaa ja parannetaan testikattavuutta.
Myös julkaisemiseen liittyy oma testausprosessinsa. Jotta julkaistava versio voidaan päästää käyttäjille asti, se on testattava asennuspakettina. Tämä varmistaa, että asennuspaketti on toimiva (aina ei ole ollut). Premiumissa tämä tapahtuu julkaisemalla uusi versio versionumerolla 0. Ilmaisversiota testataan tagaamalla uusi versio GitHubiin, jolloin se voidaan asentaa Composerilla.
Minulla on testihakemisto, jossa on tällainen composer.json
:
{ "repositories": [ { "type": "package", "package": { "name": "relevanssi/relevanssi-premium", "version": "0", "type": "wordpress-plugin", "dist": { "type": "zip", "url": "https://www.relevanssi.com/update/get_version.php?api_key=123456&version=0" } } }, { "type": "vcs", "url": "https://github.com/msaari/relevanssi" } ], "require": { "msaari/relevanssi":"4.3.4", "relevanssi/relevanssi-premium": "*" } }
Tällä asentuu sekä ilmaisversion tuorein julkaisu GitHubista että Premiumin testiversio. Sen jälkeen voin ajaa tämän skriptin:
#!/bin/sh rm -rf vendor composer clear-cache composer update cd vendor/relevanssi/relevanssi-premium/ composer update sh multi-version-test.sh cd ../../msaari/relevanssi composer update sh multi-version-test.sh
Ensin tyhjennetään vanhat versiot poistamalla vendor
-hakemisto, tyhjennetään Composerin välimuisti ja päivitetään Relevanssit.
Sitten siirrytään Relevanssi Premiumin hakemistoon, ajetaan sielläkin composer update
, joka asentaa wp-test-frameworkin
paikalleen ja sitten suoritetaan testiskripti, joka ajaa Relevanssin testit läpi useammalla WordPressin versiolla. Sen jälkeen sama toistetaan ilmaisversiolle.
Näin molemmat versiot on testattu ja mikäli kaikki testit menevät läpi kunnialla, uudet versiot voidaan julkaista, kuten prosessijutussa kuvattiin.
Testaaminen monella WordPressin versiolla
Tätä vaihetta voisi vielä avata. Aikaisemmin testasin vain yhdellä WordPressin versiolla – uusimmalla, jos olin muistanut päivittää testiasennuksen – mutta sitten keksin, että aika pienellä lisävaivalla voisi testata useammankin version: jos eri WordPress-versiot ovat omissa hakemistoissaan, riittää, että WP_TESTS_INSTALLATION
-ympäristömuuttujan arvoa vaihdetaan testien välillä.
No, innostuin sitten vähän kirjoittamaan skriptausta tehtävän automatisoimiseksi ja päädyin multi-version-test.sh-skriptiin. Se on liian pitkä tähän liitettäväksi, joten vilkuilkaa tuolta GitHubista samalla kun selitän.
Skripti edellyttää, että on määritelty hakemisto, johon WordPressit asennetaan ja josta löytyy valmiiksi säädetty wp-tests-config.php
. Sen jälkeen skriptissä määritellään, mikä on vanhin WordPressin versio, jolla halutaan testata.
Skripti käy hakemassa WordPressin API:sta tiedon saatavilla olevista versioista (tällä PHP-skriptillä), siis kunkin versiosarjan tuoreimman version. Tällä hetkellä lista menisi 5.3, 5.2.4, 5.1.3, 5.0.7, 4.9.12 ja niin edelleen.
Jokaisen testattavan version kohdalla tarkistetaan, onko se asennettu jo, eli löytyykö esimerkiksi hakemistoa wordpress-5.2.4
jo testihakemistosta. Jos ei löydy, skripti hakee tiedoston WordPressin palvelimelta wgetillä
, purkaa paketin, siirtää sen oikeaan hakemistoon ja laittaa wp-tests-config.php
-tiedoston paikoilleen.
Jos versio löytyi tai saatiin asennettua, sille ajetaan testit. Jos multisite-testien määritystiedosto löytyy (eli kyseessä on Premium) ja versio on vähintään 5.1, testit ajetaan multisitenä, muuten ajetaan yhden sivuston testit. Testit ajetaan --stop-on-failure
-parametrin kanssa, eli yksikin virhe riittää pysäyttämään testin, tämä säästää aikaa ja virheen sattuessa voi sitten halutessaan tutkia tarkemmin, mistä on kyse.
Vanhin versio, jota tällä järjestelyllä voi testata on 5.0, koska wp-test-framework sisältää asioita, jotka ovat tulleet mukaan versiossa 5.0.0. 4.9-versioita ehkä vielä pitäisi testata, sitä monet Gutenbergin pelossa vielä käyttävät (mutta kannattaisi jo pikkuhiljaa siirtyä 5-versioihin – jos et Gutenbergistä pidä, Classic Editor on tarjolla), mutta olettaisin, että 5.0 vastaa 4.9:ää riittävän hyvin.
Testauksen automatisointia
Vielä parempaa tietysti olisi, jos testausta saisi automatisoitua siten, että versionhallintaan pusketut versiot testattaisiin automaattisesti. Tällaisia CI/CD-putkiahan tarjotaan moneltakin taholta, käyttämässäni GitLabissa sellainen olisi myös tarjolla, mutta toistaiseksi olen todennut, että tässä tapauksessa testausympäristö on vaikkapa suoraviivaista Node-sovellusta sen verran monimutkaisempi rakennella, että se ylittää minun osaamiseni rajat.
Jos jollakulla sattumoisin on kokemusta WordPress-yhteensopivan testausympäristön kehittelystä vaikkapa GitLabin CI-putkeen, minuun saa olla yhteydessä. Nyt tyydyn siis toteamaan, että toimii se näinkin, ja käytän aikani mieluummin varsinaiseen kehitystyöhön kuin testausympäristön rakentelemiseen.